﻿/* Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Collections;
using System.Web;

namespace Google.Apps.Reporting
{

    public class ReportingAPIClient
    {
        #region enum

        /// <summary>
        /// Identifies the type of request that you are submitting. The only valid value for <type> tag is Report.
        /// </summary>
        public enum type
        {
            /// <summary>
            /// Report <type>
            /// </summary>
            REPORT
        }

        /// <summary>
        /// Combined with the reportName value to identify the report being retrieved.  The only valid value for the <reportType> tag is daily.
        /// </summary>
        public enum reportType
        {
            /// <summary>
            /// Daily <reportType>
            /// </summary>
            DAILY
        }

        /// <summary>
        /// Identifies the report being retrieved.
        /// </summary>
        public enum ReportName
        {
            /// <summary>
            /// The accounts report contains a list of all of the hosted accounts that exist in your domain on a particular day. 
            /// The report includes both active accounts and suspended accounts. 
            /// The status column will indicate whether each account is active or suspended.
            /// </summary>
            accounts,
            /// <summary>
            /// The activity report identifies the total number of accounts in your domain as well as the number of active and 
            /// idle accounts over several different time periods. In this report, activity encompasses user interaction with 
            /// his email, such as reading or sending email. The activity statistics includes web mail as well as POP activity.
            /// </summary>
            activity,
            /// <summary>
            /// The disk_space report shows the amount of disk space occupied by users' mailboxes. The report identifies the 
            /// total number of accounts in your domain as well as the number of accounts that fall into several different size 
            /// groupings. Mailboxes that occupy less than 1GB of disk space are grouped in increments of 100MB, and mailboxes 
            /// that occupy between 1GB and 10GB of disk space are grouped in increments of 500MB.
            /// </summary>
            disk_space,
            /// <summary>
            /// The email_clients report explains how users in your domain access their hosted accounts on a day-by-day basis. 
            /// For each day, the report lists the total number of accounts in your domain as well as the number and percentage 
            /// of users who accessed their accounts using WebMail. This report does not include suspended accounts in the account total.
            /// </summary>
            email_clients,
            /// <summary>
            /// DEPRECATED - The quota_limit_accounts report contains a list of the accounts that are approaching or have exceeded their disk space 
            /// quota. An account will be included in this report if it has less than 5 percent of its quota remaining. 
            /// This report does not include suspended accounts. - DEPRECATED
            /// </summary>
            [Obsolete]
            quota_limit_accounts,
            /// <summary>
            /// The summary report contains the total number of accounts, total mailbox usage in bytes and total mailbox quota in megabytes for your domain. 
            /// Each row in the report contains data for one day. This report does not include information for suspended accounts.
            /// </summary>
            summary,
            /// <summary>
            /// The suspended_accounts report lists all of the suspended accounts in your domain on a particular day.
            /// </summary>
            [Obsolete]
            suspended_accounts
        }
        #endregion

        #region internal variables

        /// <summary>
        /// last login with google for auth token
        /// </summary>
        private long _LastClientLoginTimeTicks = 0L; //used to track last login time for reporting services (token valid for 24h)
        private long _ticksPerDay = 864000000000L;

        /// <summar>
        /// URL to POST to obtain an authentication token
        /// </summary>
        private string _CLIENT_LOGIN_URL = "https://www.google.com/accounts/ClientLogin"; //api v1.0.1 default

        /// <summary>
        /// URL to POST to retrive resports from the API 
        /// </summary>
        private string _REPORTING_URL = "https://www.google.com/hosted/services/v1.0/reports/ReportingData"; //api v1.0.1 default

        /// <summary>
        /// Date format of the API
        /// </summary>
        private string _DATE_FORMAT = "yyyy-MM-dd"; //api v1.0.1 default

        /// <summary>
        /// Hour of the day when the API data gets published
        /// </summary>
        private int _PUBLISH_HOUR_OF_DAY = 13; //api v1.0.1 default

        /// <summary>
        /// Time diference to UTC
        /// </summary>
        private int _PUBLISH_TIME_DIFFERENCE_TO_UTC = -8; //api v1.0.1 default

        /// <summary>
        /// Email address to use for ReportingAPIClient
        /// </summary>
        private string _EMAIL = null;

        private string _PASSWORD = null;

        /// <summary>
        /// Domain FQDN to use for ReportingAPIClient
        /// </summary>
        private string _DOMAIN = null;

        /// <summary>
        /// Contains a value that Google uses to authorize access to the requested report data.
        /// </summary>
        private string _TOKEN = null;

        /// <summary>
        /// Identifies the type of account - value typically "HOSTED"
        /// </summary>
        private string _ACCOUNTTYPE = "HOSTED"; //api v1.0.1 default

        /// <summary>
        /// Identifies the Google service - typically "apps"
        /// </summary>
        private string _SERVICE = "apps"; //api v1.0.1 default

        #endregion

        #region props

        /// <summary>
        /// get the last DateTime that authentication token was obtained
        /// </summary>
        public DateTime LastClientLoginTime { get { return new DateTime(_LastClientLoginTimeTicks); } }

        /// <summary>
        /// URL to POST to obtain an authentication token
        /// </summary>
        public string CLIENT_LOGON_URL { get { return _CLIENT_LOGIN_URL; } }

        /// <summary>
        /// URL to POST to retrive resports from the API 
        /// </summary>
        public string REPORTING_URL { get { return _REPORTING_URL; } }

        /// <summary>
        /// Date format of the API
        /// </summary>
        public string DATE_FORMAT { get { return _DATE_FORMAT; } }

        /// <summary>
        /// Hour of the day when the API data gets published
        /// </summary>
        public int PUBLISH_HOUR_OF_DAY
        {
            get { return _PUBLISH_HOUR_OF_DAY; }
            set
            {
                if (value > -1 && value < 24) _PUBLISH_HOUR_OF_DAY = value;
                else throw new ArgumentOutOfRangeException("Value for PUBLISH_HOUR_OF_DAY must be int 0<=n<=23");
            }
        }

        /// <summary>
        /// Time diference to UTC
        /// </summary>
        public int PUBLISH_TIME_DIFFERENCE_TO_UTC { get { return _PUBLISH_TIME_DIFFERENCE_TO_UTC; } 
            set
            {
                if (value > -13 && value < 13) _PUBLISH_TIME_DIFFERENCE_TO_UTC = value;
                else throw new ArgumentOutOfRangeException("Value for PUBLISH_TIME_DIFFERENCE_TO_UTC must be int -12<=n<=12");
            } 
        }

        /// <summary>
        /// Email address to use for ReportingAPIClient
        /// </summary>
        public string EMAIL { get { return _EMAIL; } set { _EMAIL = value; } }

        /// <summary>
        /// Password to use for ReportingAPIClient
        /// SET ONLY - TODO: properly secure this
        /// </summary>
        public string PASSWORD { set { _PASSWORD = value; } }

        /// <summary>
        /// DDomain FQDN to use for ReportingAPIClient
        /// </summary>
        public string DOMAIN
        {
            get
            {
                if (_DOMAIN != null) return _DOMAIN;
                else { _DOMAIN = GetAdminEmailDomain(_EMAIL); return _DOMAIN; }
            }
            set { _DOMAIN = value; }
        }

        /// <summary>
        /// Contains a value that Google uses to authorize access to the requested report data.
        /// will throw TimeoutException if token has expired (older than 24h)
        /// </summary>
        public string TOKEN { 
            get 
            {
                if ((DateTime.Now.Ticks - _LastClientLoginTimeTicks) > _ticksPerDay)
                {
                    throw new TimeoutException("Token has expired.  use Google.Apps.Reporting.ReportingAPIClient.ClientLogin() to reobtain");
                }
                else
                {
                    return _TOKEN;
                }
            } 
            set { _TOKEN = value; } }

        /// <summary>
        /// Identifies the type of account - value typically "HOSTED"
        /// </summary>
        public string ACCOUNTTYPE { get { return _ACCOUNTTYPE; } set { _ACCOUNTTYPE = value; } }

        /// <summary>
        /// Identifies the Google service - typically "apps"
        /// </summary>
        public string SERVICE { get { return _SERVICE; } set { _SERVICE = value; } }

        #endregion

        #region helper methods

        /// <summary>
        /// Get the domain of the admin's email address from substring passed EMAIL parameter. 
        /// is domain component of passed smtp address
        /// </summary>
        /// <param name="EMAIL">SMTP address to examine</param>
        /// <returns>the domain, otherwise returns null</returns>
        public static string GetAdminEmailDomain(string EMAIL)
        {
            if (EMAIL != null)
            {
                int atIndex = EMAIL.IndexOf('@');
                if (atIndex > 0 && atIndex + 1 < EMAIL.Length)
                {
                    return EMAIL.Substring(atIndex + 1);
                }
            }
            else
            {
                throw new InvalidOperationException(
                    "Invalid Email - cannot generate domain from passed EMAIL. if using Get Accessor on DOMAIN property either directly SET DOMAIN or set EMAIL first to allow autogen");
            }
            return null;  
        }

        /// <summary>
        /// Get latest available report date, 
        /// based on report service time zone.
        /// Reports for the current date are available after 12:00 PST (-8 UTC) 
        /// the following day.
        /// </summary>
        /// <returns>Lastest date available</returns>
        public DateTime getLatestReportDate()
        {
            if (DateTime.UtcNow.AddHours(PUBLISH_TIME_DIFFERENCE_TO_UTC).Hour
              < PUBLISH_HOUR_OF_DAY)
            {
                return DateTime.Now.AddDays(-2);
            }
            else
            {
                return DateTime.Now.AddDays(-1);
            }
        }

        /// <summary>
        /// Checks for errors on the Http Response, errors are on XML format.
        /// When the response is xml throws an Exception with the xml text.
        /// </summary>
        /// <param name="firstLine">
        ///   First line of the StreamReader from the Http Response</param>
        /// <param name="reader">StreamReader from the Http Response</param>
        private static void checkError(string firstLine, StreamReader reader)
        {
            if (firstLine.Trim().StartsWith("<?xml"))
            {
                String xmlText = firstLine;
                while (reader.Peek() >= 0)
                {
                    xmlText += reader.ReadLine();
                }
                throw new Exception(xmlText);
            }
        }

        #endregion

        #region obtain report methods

        /// <summary>
        /// Creates a XML request for the Report
        /// </summary>
        /// <param name="reportName">The name of the Report as represented by reportType enum: activity, disk_space, email_clients, quota_limit_accounts, summary, suspended_account</param>
        /// <param name="date">string of date of the Report</param>
        /// <returns>XML request as a string</returns>
        private string createRequestXML(ReportName reportName, string date)
        {
            string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
            xml += "<rest xmlns=\"google:accounts:rest:protocol\"";
            xml += " xmlns:xsi=\"";
            xml += "http://www.w3.org/2001/XMLSchema-instance\">";
            xml += "<type>Report</type>";
            xml += "<token>" + this.TOKEN + "</token>";  //will throw exception if token expired (age > 24h)
            xml += "<domain>" + this.DOMAIN + "</domain>"; //will autogen from EMAIL if this is non-null / 0 length - will throw exception if null or 0 length and cannot autogen
            xml += "<date>" + date + "</date>";  //date string should be formatted by GetReport methods
            xml += "<reportType>daily</reportType>";
            xml += "<reportName>" + reportName.ToString() + "</reportName>";
            xml += "</rest>";
            return xml;
        }

        /// <summary>
        /// Obtain report - *CAST RETURN TO APPROPRIATE REPORT TYPE*
        /// </summary>
        /// <param name="reportName">Report to obtain</param>
        /// <param name="date">Obtain report for this date.  Pass null for latest report.</param>
        /// <returns>object - *CAST RETURN TO APPROPRIATE REPORT TYPE*</returns>
        public object getReport(ReportName reportName, string date)
        {

            AccountsReport accountsReport = null;
            ActivityReport activityReport = null;
            DiskSpaceReport diskSpaceReport = null;
            EmailClientsReport emailClientsReport = null;
            QuotaLimitAccountsReports quotaLimitAccountsReport = null;
            SummaryReport summaryReport = null;
            SuspendedAccountsReport suspendedAccountsReport = null;

            if (date == null)
            {
                date = getLatestReportDate().ToString(_DATE_FORMAT);
            }
            else
            {
                try
                {
                    date = System.Convert.ToDateTime(date).ToString
                      (_DATE_FORMAT);
                }
                catch
                {
                    throw new ArgumentException("Invalid Date");
                }
            }

            string xml = createRequestXML(reportName, date);
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(REPORTING_URL);
            request.Method = "POST";
            UTF8Encoding encoding = new UTF8Encoding();
            byte[] postBuffer = encoding.GetBytes(xml);
            request.ContentLength = postBuffer.Length;
            request.ContentType = "application/x-www-form-urlencoded";
            Stream requestStream = request.GetRequestStream();
            requestStream.Write(postBuffer, 0, postBuffer.Length);
            requestStream.Close();
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            StreamReader reader = new StreamReader(response.GetResponseStream());
            String firstLine = null;
            String lineBuffer = String.Empty;
            if (reader.Peek() >= 0)
            {
                firstLine = reader.ReadLine();
                checkError(firstLine, reader);
            }

            switch (reportName)
            {
                case ReportName.accounts:
                    accountsReport = new AccountsReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                        accountsReport.Add(new AccountsReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return accountsReport;
                case ReportName.activity:
                    activityReport = new ActivityReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       activityReport.Add(new ActivityReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return activityReport;
                case ReportName.disk_space:
                    diskSpaceReport = new DiskSpaceReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       diskSpaceReport.Add(new DiskSpaceReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return diskSpaceReport;
                case ReportName.email_clients:
                    emailClientsReport = new EmailClientsReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       emailClientsReport.Add(new EmailClientsReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return emailClientsReport;
                case ReportName.quota_limit_accounts:
                    quotaLimitAccountsReport = new QuotaLimitAccountsReports();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       quotaLimitAccountsReport.Add(new QuotaLimitAccountsReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return quotaLimitAccountsReport;
                case ReportName.summary:
                    summaryReport = new SummaryReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       summaryReport.Add(new SummaryReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return summaryReport;
                case ReportName.suspended_accounts:
                    suspendedAccountsReport = new SuspendedAccountsReport();
                    while ((lineBuffer = reader.ReadLine()) != null)
                    {
                       suspendedAccountsReport.Add(new SuspendedAccountsReportRecord(lineBuffer));
                    }
                    reader.Close();
                    return suspendedAccountsReport;
                default:
                    throw new FormatException("reached default case for report generation, this should not occur");
            }

        }

        #endregion

        #region authentication methods

        /// <summary>
        /// Logs in the user and initializes the Token
        /// </summary>
        public void ClientLogin(string email, string password, string accountType, string service, string CLIENT_LOGIN_URL)
        {

            //set last token get time to now
            _LastClientLoginTimeTicks = DateTime.Now.Ticks;

            string token = null;
            UTF8Encoding encoding = new UTF8Encoding();
            string postData = "Email=" + HttpUtility.UrlEncode(email) +
              "&Passwd=" + HttpUtility.UrlEncode(password) +
              "&accountType=" + HttpUtility.UrlEncode(accountType) +
              "&service=" + HttpUtility.UrlEncode(service);
            byte[] data = encoding.GetBytes(postData);
            HttpWebRequest request =
              (HttpWebRequest)WebRequest.Create(CLIENT_LOGIN_URL);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = data.Length;
            Stream inputStream = request.GetRequestStream();
            inputStream.Write(data, 0, data.Length);
            inputStream.Close();
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            string responseStr = (new StreamReader(
              response.GetResponseStream())).ReadToEnd();
            char[] tokenizer = { '\r', '\n' };
            string[] parts = responseStr.Split(tokenizer);
            foreach (string part in parts)
            {
                if (part.StartsWith("SID="))
                {
                    token = part.Substring(4);
                    break;
                }
            }
            this._TOKEN = token;
        }

        #endregion

        #region instantiation

        /// <summary>
        /// default instantiation
        /// </summary>
        public ReportingAPIClient() { }

        /// <summary>
        /// instantiation of ReportingAPIClient - uses default values specified in API v1.0.1 for ACCOUNTTYPE, CLIENT_LOGIN_URL, DATE_FORMAT, PUBLISH_HOUR_OF_DAY,
        /// PUBLISH_TIME_DIFFERENCE_TO_UTC, REPORTING_URL, SERVICE
        /// </summary>
        /// <param name="EMAIL">email address to use for authentication with google reporting services</param>
        /// <param name="PASSWORD">password to use</param>
        public ReportingAPIClient(string EMAIL, string PASSWORD)
        {
            //other needed variables, really constants per API v1.0.1 set at instantiation directly on internal variables (see internal variables region of this class)
            _EMAIL = EMAIL;
            _DOMAIN = GetAdminEmailDomain(EMAIL);
            _PASSWORD = PASSWORD;
        }

        /// <summary>
        /// instantiation of ReportingAPIClient using specified values.  use ReportingAPIClient(string EMAIL, string PASSWORD) to instantiate with default constant 
        /// values specified API v1.0.1
        /// </summary>
        /// <param name="ACCOUNTTYPE">default - HOSTED</param>
        /// <param name="CLIENT_LOGIN_URL">Client login URL for google reporting services.  default - https://www.google.com/accounts/ClientLogin </param>
        /// <param name="DATE_FORMAT">default - yyyy-MM-dd</param>
        /// <param name="DOMAIN">FQDN of domain to use - api default generates this from passed EMAIL - passing null for this parameter will cause autogen attempt from EMAIL parameter</param>
        /// <param name="EMAIL">smtp address of user for authentication against google reporting services</param>
        /// <param name="PASSWORD">password to use in reporting services authentication</param>
        /// <param name="PUBLISH_HOUR_OF_DAY">24h hour of day.  default - 13</param>
        /// <param name="PUBLISH_TIME_DIFFERENCE_TO_UTC">time offset of report timestamps.  default - -8</param>
        /// <param name="REPORTING_URL">url to google reporting service.  default - https://www.google.com/hosted/services/v1.0/reports/ReportingData </param>
        /// <param name="SERVICE">default - apps</param>
        public ReportingAPIClient(string ACCOUNTTYPE, string CLIENT_LOGIN_URL, string DATE_FORMAT, string DOMAIN, 
            string EMAIL, string PASSWORD, int PUBLISH_HOUR_OF_DAY, int PUBLISH_TIME_DIFFERENCE_TO_UTC, string REPORTING_URL, string SERVICE)
        {

            if (EMAIL == null || EMAIL.Length == 0) throw new ArgumentNullException("EMAIL cannot be null or 0 length");
            if (DOMAIN == null || DOMAIN.Length == 0) _DOMAIN = GetAdminEmailDomain(EMAIL);
            if (PASSWORD == null || PASSWORD.Length == 0) throw new ArgumentNullException("PASSWORD cannot be null or 0 length");
            if (CLIENT_LOGIN_URL == null || CLIENT_LOGIN_URL.Length == 0) throw new ArgumentNullException("CLIENT_LOGIN_URL value cannot be null or 0 length");
            if (REPORTING_URL == null || REPORTING_URL.Length == 0) throw new ArgumentNullException("REPORTING_URL value cannot be null or 0 length");

            _EMAIL = EMAIL;
            _DOMAIN = DOMAIN;
            _PASSWORD = PASSWORD;
            _ACCOUNTTYPE = ACCOUNTTYPE;
            _CLIENT_LOGIN_URL = CLIENT_LOGIN_URL;
            _DATE_FORMAT = DATE_FORMAT;            
            this.PUBLISH_HOUR_OF_DAY = PUBLISH_HOUR_OF_DAY;  //use SET accessor on property to ensure bounds enforcement
            this.PUBLISH_TIME_DIFFERENCE_TO_UTC = PUBLISH_TIME_DIFFERENCE_TO_UTC; //use SET accessor on property to ensure bounds enforcement
            _REPORTING_URL = REPORTING_URL;
            _SERVICE = SERVICE;
        }

        #endregion

    }
}
